package ch.zhaw.skuh.turing.model.turingmaschine;

public final class Maschine {

	// rule-set structure: 3D-array (complicated, but rather fast)
	// x-dimension: state
	// y-dimension: tape0*alphSize^0 + tape1*alphSize^1 + ...
	// z-dimension: resulting rule, by positions [x]
	// z-[0]: hasRule-indicator (!=0 means rule exists)
	// z-[1]: new state
	// z-[2 to 2+tapeCount]: move for tapes-#
	// z-[2+tapecount .... 2+2*tapecount]: new char for tapes-# (again, char as
	// int-positions in alphabet)
	// ==> searching for rule means checking if [x][y][0] is != 0
	private byte[][][] ruleSet;

	private int lengthAlphabet = 0;

	// the tapes, stored are only positions of char in alphabet !!
	private byte[][] tapes;

	// positions in tapes
	private int[] positions;

	// the stateCurrent of the machine
	private int stateCurrent = 0;

	// step count
	private long steps = 0;

	private final int MAXARRAY_LENGTH = (int) Math.pow(2, 25);
	private final int POSITION_OFFSET = MAXARRAY_LENGTH / 2;

	private int[] radixMap;

	// TODO: Make temp-vars --> instance-vars for performance (try it)

	protected Maschine(Turing op, int[][] input, int[] pos, int startState) {
		this.lengthAlphabet = op.getAlphLength();
		this.positions = new int[op.getTapeCount()];
		this.tapes = new byte[op.getTapeCount()][MAXARRAY_LENGTH];

		fillRadixMap();

		for (int i = 0; i < op.getTapeCount(); i++) {
			this.positions[i] = POSITION_OFFSET + pos[i];
		}
		// check
		if (pos.length > this.tapes.length) {
			throw new ArrayIndexOutOfBoundsException(pos.length);
		}
		this.ruleSet = new byte[op.getMaxState()][(int) Math.pow(lengthAlphabet,
				tapes.length)][2 + 2 * tapes.length];
		setTapes(input, startState);
	}

	// add rule:
	// watch rule-definition-comment at top!!
	protected void addRule(int stateOld, int[] tapesRead, int stateNew,
			int[] tapesWrite, int[] moves) {
		// checks
		if (tapesWrite.length != tapes.length || moves.length != tapes.length
				|| stateOld >= ruleSet.length || stateNew >= ruleSet.length) {
			throw new IndexOutOfBoundsException();
		}
		checkTape(tapesRead);
		checkTape(tapesWrite);

		int powerIndex = toPowerIndex(tapesRead);
		// check if rule exists
		if (hasRule(stateOld, powerIndex)) {
			// rule already exists --> error
			StringBuilder sbError = new StringBuilder(
					"rule already exists --> error: q");
			sbError.append(Integer.toString(stateOld));
			sbError.append(", tapes in alph-positions: ");
			for(int i=0;i<tapesRead.length;i++){
				sbError.append("[" + Integer.toString(tapesRead[i]) + "]");
			}
			throw new IndexOutOfBoundsException(sbError.toString());
		}
		// store hasRule
		ruleSet[stateOld][powerIndex][0] = 1;
		// store new state
		ruleSet[stateOld][powerIndex][1] = (byte) stateNew;
		// store moves
		for (int i = 0; i < tapes.length; i++) {
			ruleSet[stateOld][powerIndex][2 + i] = (byte) moves[i];
		}
		// store new char(-positions)
		for (int i = 0; i < tapes.length; i++) {
			ruleSet[stateOld][powerIndex][2 + tapes.length + i] = (byte) tapesWrite[i];
		}
	}

	private void checkTape(int[] tapeArray) {
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			if (tapeArray[iTape] < 0 || tapeArray[iTape] >= lengthAlphabet) {
				throw new IndexOutOfBoundsException();
			}
		}
	}

	protected boolean hasRule(int stateRead, int powerIndex) {
		return (ruleSet[stateRead][powerIndex][0] != 0);
	}

	public boolean hasRule() {
		// int powerIndex = getPowerIndex();
		return hasRule(stateCurrent, getPowerIndex());
	}

	public boolean hasRule(int state, int[] tapeRead) {
		// check input
		if (tapeRead.length != tapes.length) {
			throw new IndexOutOfBoundsException();
		}
		checkTape(tapeRead);
		int powerIndex = toPowerIndex(tapeRead);
		return hasRule(state, powerIndex);
	}

	private int getPowerIndex() {
		// coded out for speed
		int index = 0;
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			index += tapes[iTape][positions[iTape]] * radixMap[iTape];
		}
		return index;
	}

	private void fillRadixMap() {
		radixMap = new int[tapes.length];
		for (int i = 0; i < radixMap.length; i++) {
			radixMap[i] = (int) Math.pow(lengthAlphabet, i);
		}
	}

	// this is unchecked for integers in alph!!
	private int toPowerIndex(int[] tapesRead) {
		// checks
		if (tapesRead.length != tapes.length) {
			throw new IndexOutOfBoundsException();
		}
		int index = 0;
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			// int read = tapesRead[iTape];
			// int pow = (int) Math.pow(lengthAlphabet, iTape);
			index += tapesRead[iTape] * radixMap[iTape];
		}
		return index;
	}

	public void run() {
		int powerIndex = getPowerIndex();
		do {
			byte[] operation = ruleSet[stateCurrent][powerIndex];

			// check rule exists
			if (operation[0] != 0) {
				for (int iTape = 0; iTape < tapes.length; iTape++) {
					// moves are stored first, but we need to set chars first!!
					tapes[iTape][positions[iTape]] = operation[2 + tapes.length
							+ iTape];
					// do move
					positions[iTape] += operation[2 + iTape];
				}
				// set new stateCurrent
				stateCurrent = operation[1];
				steps++;

				// set new powerIndex
				powerIndex = getPowerIndex();
			} else {
				powerIndex = -1;
			}
		} while (powerIndex >= 0);
	}

	protected boolean doSaveStep() {
		if (hasRule()) {
			doStep();
		} else {
			return false;
		}
		return hasRule();
	}

	private void doStep() {
		// warning! this assumes that rule-existence has been checked
		// get stateCurrent before access gets lost
		
		// coded out for speed
		int index = 0;
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			index += tapes[iTape][positions[iTape]] * radixMap[iTape];
		}
		
		byte[] operation = ruleSet[stateCurrent][index];
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			// moves are stored first, but we need to set chars first!!
			tapes[iTape][positions[iTape]] = operation[2 + tapes.length + iTape];
			// do move
			positions[iTape] += operation[2 + iTape];
		}
		// set new stateCurrent
		this.stateCurrent = operation[1];
		steps++;
	}

	// getter/setter
	protected int[] getRule(int state, int[] tapeRead) {
		// check input
		if (tapeRead.length != tapes.length) {
			throw new IndexOutOfBoundsException();
		}
		checkTape(tapeRead);
		int powerIndex = toPowerIndex(tapeRead);
		
		return getByteToInt(ruleSet[state][powerIndex]);
	}
	
	private int[] getByteToInt(byte[] b){
		int[] ret = new int[b.length];
		for(int i=0;i<ret.length;i++){
			ret[i] = b[i];
		}
		return ret;
	}

	private void setTapes(int[][] input, int startState) {
		if (input.length != tapes.length) {
			throw new IndexOutOfBoundsException();
		}
		for (int i = 0; i < input.length; i++) {
			// clear tape
			this.tapes[i] = new byte[MAXARRAY_LENGTH];
			// tape can be null/empty
			if (input[i] != null) {
				for (int j = 0; j < input[i].length; j++) {
					// check each char
					if (input[i][j] < 0 || input[i][j] >= lengthAlphabet) {
						throw new IndexOutOfBoundsException();
					}
					// checks ok, store
					this.tapes[i][POSITION_OFFSET + j] = (byte) input[i][j];
				}
			}
		}
		stateCurrent = startState;
		steps = 0;
	}

	public int getPosition(int tape) {
		return positions[tape] - POSITION_OFFSET;
	}

	public int getState() {
		return stateCurrent;
	}

	protected int[] getTape(int tape) {
		return getByteToInt(tapes[tape]);
	}

	protected int getTapeAtPosition(int tape, int index) {
		return tapes[tape][positions[tape] + index];
	}

	protected int[] getTapesAtCurrent() {
		int[] retTape = new int[tapes.length];
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			retTape[iTape] = tapes[iTape][positions[iTape]];
		}
		return retTape;
	}

	protected int[][] getTapesPart(int offset) {
		int[][] retTapes = new int[tapes.length][2 * offset + 1];
		for (int iTape = 0; iTape < tapes.length; iTape++) {
			for (int i = 0; i < 2 * offset + 1; i++) {
				retTapes[iTape][i] = getTapeAtPosition(iTape, -offset + i);
			}
		}
		return retTapes;
	}

	public long getSteps() {
		return steps;
	}
}
